//-----------------------------------------------------------------------------------------------
// Author: Andre Van Der Merwe <dart@pobox.com>
// Project: .net Compound File (http://dotnetpcf.sourceforge.net)
//
// Modifications
//    2001-02-13: (AVDM) AllocTable.SaveToStream is now called only once per save. SaveHashtableEntries
//                added for use by SaveToStream. Before this change SaveToStream was taking +5.37%
//                of the time for a save (8mb) after the change it took 0.0065%.
//    2001-02-13: (AVDM) Started adding support for extents to the SaveToStream function
//                 i.e 5 consecutive linked blocks saved as a single block
//    2001-02-14: (AVDM) Removed extents support. On an 8mb test file with the following alloc table
//                 entries 1,2,3,4,5-4223 (i.e. 5-4423 is an extent) approx 100kb was saved by
//                 using extents, however time to save went from 0.0065% to 2%. The space savings
//                 did not justify the additional time overhead - and this is on a perfect case,
//                 i.e. almost 8mb of sequential data, less space would be saved in a "normal case"
//    2001-02-14: (AVDM) AllocTable.SaveToStream required some fixing (update header count + passing 
//                 currentHeader and iLastBlockStartPos as reference params
//
// Copyright (c) Andre Van Der Merwe 2000-2001
// Licence: BSD - see the licence.txt file or visit the project homepage
//-----------------------------------------------------------------------------------------------


namespace dotnetpcf.CompoundFile
{
   using System;
   using System.Diagnostics;
   using System.Collections;
   using System.Data;
   using System.IO;

   public enum AllocTableFlags : uint
   {
      efFree = 0x00, // Must be used on own i.e. do an equality check not a bitwise comparison
      efSystem = 0x01,
      efCFHeader = 0x02,
      efAllocTable = 0x04,
      efFileTable = 0x08,
      efDirTable = 0x10,
      efUser = 0x20,
      efNames = 0x40,
      efDirProperty = 0x80,
      efFileProperty = 0x100
   }



   /// <summary>
   /// Each block in the file is described by a single entry in the alloc table
   /// </summary>
   /// <remarks>
   /// There can be more than one allocation table block in a compound file. Since the allocation
   /// table is loaded before the file table, the FSFilterStream can not be used to load the alloc
   /// table entries. For this reasone the AllocTableHeader has a m_iNext member which contains the
   /// block index to the next allocation table block. 
   /// 
   /// Allocation table entries can be chained to create large virtual blocks of data (e.g. files)
   /// Each allocation table entry has a m_iNext member which is used to chain entries
   /// </remarks>
   internal class AllocTable
   {
      public class AllocTableHeader : BlockHeader 
      {
         public new const uint SIZE = BlockHeader.SIZE + 4;
         public uint m_iNext = SysInfo.UNDEFINED;

         public override void LoadFromStream(  Stream strm  )
         {
            base.LoadFromStream(  strm  );

            BinaryReader rdr = new BinaryReader(  strm  );
            m_iNext = rdr.ReadUInt32(); 
         }

         public override void SaveToStream(  Stream strm  )
         {
            base.SaveToStream(  strm  );

            BinaryWriter wrtr = new BinaryWriter(  strm  );
            wrtr.Write(  m_iNext  );
         }
      }

      /// <summary>
      /// Single allocation table entry
      /// </summary>
      internal class AllocTableEntry
      {
         public const uint SIZE = 12;

         public uint m_iID = SysInfo.UNDEFINED;
         public uint m_iNext = SysInfo.UNDEFINED;
         public uint m_iFlags = (uint)AllocTableFlags.efFree;

         public void SaveToStream(  Stream strm  )
         {
            Debug.Assert(  null != strm  );
            if(  null == strm)
               throw(  new CompoundFileException(  CompoundFileResult.cfrStreamIsNull  )  );

            BinaryWriter wrtr = new BinaryWriter(  strm  );

            wrtr.Write(  m_iID  );
            wrtr.Write(  m_iNext  );
            wrtr.Write(  m_iFlags  );
         }

         public void LoadFromStream(  Stream strm  )
         {
            Debug.Assert(  null != strm  );
            if(  null == strm  )
               throw(  new CompoundFileException(  CompoundFileResult.cfrStreamIsNull  )  );

            BinaryReader rdr = new BinaryReader(  strm  );

            m_iID = rdr.ReadUInt32();
            m_iNext = rdr.ReadUInt32();
            m_iFlags = rdr.ReadUInt32();
         }
      }

      public AllocTable(  CompoundFileSys cf  )
      {
         m_cfParent = cf;
         m_Header.m_iMagic = SysInfo.MAGIC_ALLOCTBL_HEADER;

         m_EntriesOnChangeCallBack = new NotifyHashtable.OnChangeCallBack(  Entries_Modified  );
         m_Entries.OnChange = m_EntriesOnChangeCallBack;
      }                       

      private bool m_bModified = false;
      private CompoundFileSys m_cfParent;
      private AllocTableHeader m_Header = new AllocTableHeader();
      private NotifyHashtable m_Entries = new NotifyHashtable();
      private NotifyHashtable.OnChangeCallBack m_EntriesOnChangeCallBack = null;

      private void Entries_Modified()
      {
         m_bModified = true;
      }

      /// <summary>
      /// Loads the allocation table from a stream
      /// </summary>
      /// <param name="strm"> </param>
      public void LoadFromStream(  Stream strm  )
      {
         Debug.Assert(  null != strm  );
         Debug.Assert(  strm.GetType() != typeof(FSFilterStream)  );

         if(  null == strm  )
            throw(  new CompoundFileException(  CompoundFileResult.cfrStreamIsNull  )  );

         m_bModified = false;

         long iStartPos = strm.Position;
         m_Header.LoadFromStream(  strm  );

         if(  SysInfo.MAGIC_ALLOCTBL_HEADER != m_Header.m_iMagic  )
            throw(  new CompoundFileException(  CompoundFileResult.cfrInvalidAllocTableMagicNumbers  )  );

         long iLastBlockStartPos = iStartPos;
         uint iCurrentPhysicalBlock = m_cfParent.Header.m_iAllocTableIndex;
         AllocTableHeader currentHeader = m_Header;

            //Load all entries 
         for(  int i = 0;  i < m_Header.m_iTotalItems;  ++i  )
         {
            long iLeft = SysInfo.BLOCK_BYTE_SIZE - (strm.Position - iLastBlockStartPos) - AllocTableHeader.SIZE;

            if(  iLeft < AllocTableEntry.SIZE  )
            {
               Debug.Assert(  SysInfo.UNDEFINED != currentHeader.m_iNext,  "Alloc table - invalid chaning - too few entries"  );

                  //Thre should be a next block
               if(  SysInfo.UNDEFINED == currentHeader.m_iNext  )
                  throw(  new CompoundFileException(  CompoundFileResult.cfrLoadAllocTableFailedChainingInvalid  )  );

                  //Move to next block and update variables
               iCurrentPhysicalBlock = currentHeader.m_iNext;
               strm.Position = iCurrentPhysicalBlock * SysInfo.BLOCK_BYTE_SIZE;
               iLastBlockStartPos = strm.Position;

                  //read the header - could just seek past...
               currentHeader = new AllocTableHeader();
               currentHeader.LoadFromStream(  strm  );
               Debug.Assert(  SysInfo.MAGIC_ALLOCTBL_HEADER == currentHeader.m_iMagic  );
            }
            
            AllocTableEntry ent = new AllocTableEntry();
            ent.LoadFromStream(  strm  );
            Debug.Assert(  SysInfo.UNDEFINED != ent.m_iID,  "m_iID is undefined for a alloc table entry"  );

            m_Entries.Add(  ent.m_iID,  ent  ); 
         }

         //Check that num = header.m_iTotalItems
         Debug.Assert(  m_Header.m_iTotalItems == m_Entries.Count  );
      }

      /// <summary>
      /// Saves the allocation table to the file. This is a rather complex routine
      /// as it needs to allocate new while saving the table. Other classes will use
      /// a FSFilterStream to hide this complexity, but this is not possible with
      /// the Allocation table. 
      /// 
      /// The reson is that the allocation table must be loaded before a FSFilterStream
      /// can be used. This is because a FSFilterStream used the allocation table to move
      /// from one block to the next. 
      /// 
      /// This method saves the allocation table and stores a "next pointer" in each
      /// alloc table block that is allocated. When the alloc table is loaded the load
      /// method can follow these "pointers" and load the entire table
      /// 
      /// NB note that while saving the stream more alloc entries can be created, 
      /// these entries are stored in a temp list and stored last
      /// </summary>
      /// <param name="strm">A physical stream, not a FSFilterStream!</param>
      public void SaveToStream(  Stream strm  )
      {
         Debug.Assert(  null != strm  );
         Debug.Assert(  strm.GetType() != typeof(FSFilterStream)  );

         if(  null == strm  )
            throw(  new CompoundFileException(  CompoundFileResult.cfrStreamIsNull  )  );

         long iStartPos = strm.Position;
         long iHeaderPos = iStartPos;
         m_Header.m_iTotalItems = (uint)m_Entries.Count; 
         m_Header.SaveToStream(  strm  );

         Hashtable TempNewEntries = new Hashtable();
         Hashtable TempSaveEntries = new Hashtable();

         AllocTableHeader currentHeader = m_Header;
            //Save the exisint entries
         SaveEntries(  ref iStartPos,  m_Entries.GetEnumerator(),  ref TempNewEntries,  ref currentHeader,  strm  );

            //Save any new entried that were created
         while(  0 < TempNewEntries.Count  )
         {
            IDictionaryEnumerator enumTemp = TempNewEntries.GetEnumerator();

            while(  enumTemp.MoveNext()  )
               m_Entries.Add(  ((AllocTableEntry)enumTemp.Value).m_iID,  ((AllocTableEntry)(enumTemp.Value))  );

            TempSaveEntries = (Hashtable)TempNewEntries.Clone();
            TempNewEntries.Clear();

            SaveEntries(  ref iStartPos,  TempSaveEntries.GetEnumerator(),  ref TempNewEntries,  ref currentHeader,  strm  );
         }

         /////////////////////////////////////////////////////////////////////
         // The number of alloc table items could have been changes, so save 
         //  an updated count
         /////////////////////////////////////////////////////////////////////
         long iCurrentPos = strm.Position;
         strm.Position = iHeaderPos;
         m_Header.m_iTotalItems = (uint)m_Entries.Count; 
         m_Header.SaveToStream(  strm  );
         strm.Position = iCurrentPos;

         m_bModified = false;
      }

      /// <summary>
      /// Helper for SaveToStream. Any new allocation table blocks that are created while saving the 
      /// allocation table are stored in the NewEntries list. These items must then be passed in the
      /// EntriesToSaveEnum to this method. This must happen untill no more items have been added to
      /// NewEntries
      /// </summary>
      /// <param name="iLastBlockStartPos"> </param>
      /// <param name="EntriesToSaveEnum">Current entries to save</param>
      /// <param name="NewEntries">List in which new entries will be saved</param>
      /// <param name="currentHeader">Current alloc table header</param>
      /// <param name="strm">Stream to save to</param>
      private void SaveEntries(  ref long iLastBlockStartPos,  
                                 IDictionaryEnumerator EntriesToSaveEnum,  
                                 ref Hashtable NewEntries,  
                                 ref AllocTableHeader currentHeader,
                                 Stream strm  
                               )
      {
         Debug.Assert(  null != strm  );

         uint iCurrentPhysicalBlock = m_cfParent.Header.m_iAllocTableIndex;
         AllocTableEntry ate = (AllocTableEntry)m_Entries[ iCurrentPhysicalBlock ];
         Debug.Assert(  null != ate,  "unable to find physical block"  );

            //First free alloc table block - declaring out of while loop increases performance
         uint iFirstFree = 0;

         while(  EntriesToSaveEnum.MoveNext()  )
         {
               //Number of bytes that can still be written to this block
            long iLeft = SysInfo.BLOCK_BYTE_SIZE - (strm.Position - iLastBlockStartPos) - AllocTableHeader.SIZE;

               //Need to move to next block?
            if(  iLeft < AllocTableEntry.SIZE  )
            {
                  //Is there already a "next" block? If so the data in it can be overwritten.
                  //  If not a new block will need to be allocated
               if(  SysInfo.UNDEFINED == currentHeader.m_iNext )
               {
                     //Find a free alloc table entry
                  while(  m_Entries.ContainsKey(  ++iFirstFree  )   )
                  {}

                     //Create the new alloc entry for the allocation table.
                     // This new entry is stored in the NewEntries list and must
                     // be saved with another call to SaveEntries once it has finished
                  AllocTableEntry nextAllocEntry = new AllocTableEntry();
                  nextAllocEntry.m_iFlags = ate.m_iFlags;
                  nextAllocEntry.m_iNext = SysInfo.UNDEFINED;
                  nextAllocEntry.m_iID = iFirstFree;
                  NewEntries.Add(  iFirstFree,  nextAllocEntry  );

                     //Update the header of the current block to point to the new block
                  strm.Position = iLastBlockStartPos;
                  currentHeader.m_iNext = iFirstFree;
                  currentHeader.SaveToStream(  strm  );


                  ///////////////////
                  // Start new block
                  ///////////////////

                     //New Header
                  currentHeader = new AllocTableHeader();
                  currentHeader.m_iMagic = SysInfo.MAGIC_ALLOCTBL_HEADER;
                  currentHeader.m_iTotalItems = SysInfo.UNDEFINED;
                  currentHeader.m_iNext = SysInfo.UNDEFINED;

                     //Create space for the block if nessasary
                  if(  strm.Length < (iFirstFree + 1) * SysInfo.BLOCK_BYTE_SIZE  )
                  {
                     strm.SetLength(  (iFirstFree + 1) * SysInfo.BLOCK_BYTE_SIZE  );
                     strm.Position = iFirstFree * SysInfo.BLOCK_BYTE_SIZE;
                  }

                     //Seek to new pos + update variables
                  iCurrentPhysicalBlock = iFirstFree;
                  iLastBlockStartPos = iCurrentPhysicalBlock * SysInfo.BLOCK_BYTE_SIZE;
                  strm.Position = iLastBlockStartPos;
                  ate = nextAllocEntry;

                     //Save the new header
                  currentHeader.SaveToStream(  strm  );

                  uint iii = ((AllocTableEntry)EntriesToSaveEnum.Value).m_iID;
               }
               else
               {
                     //Move to next block and update variables
                  iCurrentPhysicalBlock = currentHeader.m_iNext;
                  strm.Position = iCurrentPhysicalBlock * SysInfo.BLOCK_BYTE_SIZE;
                  iLastBlockStartPos = strm.Position;
                  ate = (AllocTableEntry)m_Entries[ iCurrentPhysicalBlock ];

                     //read the header
                  currentHeader = new AllocTableHeader();
                  currentHeader.LoadFromStream(  strm  );
                  Debug.Assert(  SysInfo.MAGIC_ALLOCTBL_HEADER == currentHeader.m_iMagic  );
               }
            }
            
            ((AllocTableEntry)EntriesToSaveEnum.Value).SaveToStream(  strm  );
         }
      }

      /// <summary>
      /// Create a new allocation table entry and creates space in the physical stream if requried
      /// </summary>
      /// <param name="flags">The flags for the new allocation entry</param>
      public uint GetNewAllocTableEntry(  AllocTableFlags flags  )
      {
         uint iStart = 0;
         return GetNewAllocTableEntry(  flags,  ref iStart  );
      }

      public uint GetNewAllocTableEntry(  AllocTableFlags flags,  ref uint iStartingAt  )
      {
         m_bModified = true;

            //Find the first free allocation location
         uint iFirstFree = iStartingAt;

         while(  m_Entries.ContainsKey(  ++iFirstFree  )   )
         {}

         iStartingAt = iFirstFree;

            //Create the new entry
         AllocTableEntry nextAllocEntry = new AllocTableEntry();
         nextAllocEntry.m_iFlags = (uint)flags;
         nextAllocEntry.m_iNext = SysInfo.UNDEFINED;
         nextAllocEntry.m_iID = iFirstFree;
         m_Entries.Add(  iFirstFree,  nextAllocEntry  );

            //Alloc space for this block if nessasary
         if(  ((iFirstFree + 1) * SysInfo.BLOCK_BYTE_SIZE) > m_cfParent.CompoundFileStream.Length  )
            m_cfParent.CompoundFileStream.SetLength(  (iFirstFree + 1) * SysInfo.BLOCK_BYTE_SIZE  );

         return iFirstFree;
      }

      
      /// <summary>
      /// Add another block to an existing file. This is an allocation table task, which is why
      /// this function is not in the FileTable class.
      /// Returns the ID of the new allocation block added to the file 
      /// </summary>
      /// <param name="fent">The exsisting file's file table entry</param>
      /// <param name="iNumBlocks">Number of blocks required</param>
      public bool GetNewFileBlocks(  FileTable.FileTableEntry fent,  uint iNumBlocks  )
      {
            //Get the first alloc table entry
         AllocTableEntry firstAllocEntry = (AllocTableEntry)m_Entries[ fent.m_iAllocTblIndex ];

         if(  null == firstAllocEntry  )
            return false;

         m_bModified = true;

            //Get the last item in the existing chain
         uint iLastOldBlock = fent.FileTableOwner.GetNthBlock(   fent,  fent.FileTableOwner.GetBlockCount(  fent  ) - 1   );
         Debug.Assert(  0 != iLastOldBlock  );
         AllocTableEntry lastOldAllocEntry = (AllocTableEntry)m_Entries[ iLastOldBlock ];
         Debug.Assert(  null != lastOldAllocEntry  );

         //TODO calc the num of blocks correctly
         uint iMinSize = iNumBlocks + fent.FileTableOwner.GetBlockCount(  fent  );
         iMinSize += (uint)m_Entries.Count;
         iMinSize += (uint)m_cfParent.DirTable.EntryCount;
         iMinSize += (uint)m_cfParent.FileTable.EntryCount;
         iMinSize += 2; //1 for header, one for fun
         iMinSize *= SysInfo.BLOCK_BYTE_SIZE;

         if(  iMinSize > m_cfParent.CompoundFileStream.Length  )
            m_cfParent.CompoundFileStream.SetLength(  iMinSize  );
            
         uint iStartBlock = 0;

         for(  int i = 0;  i < iNumBlocks;  ++i  )
         {
               //Get a new allocation block
            uint iNewBlockIndex = GetNewAllocTableEntry(  (AllocTableFlags)firstAllocEntry.m_iFlags,  ref iStartBlock  );

            Debug.Assert(  0 != iNewBlockIndex  );
            AllocTableEntry nextAllocEntry = (AllocTableEntry)m_Entries[ iNewBlockIndex ];
            Debug.Assert(  null != nextAllocEntry  );

               //Chain old -> new
            lastOldAllocEntry.m_iNext = nextAllocEntry.m_iID;

            lastOldAllocEntry = nextAllocEntry;
         }

            //File's alloc tbl list is now invalid
         fent.AllocBlockList = null;

         return true;
      }



      public bool Modified
      {
         get{ return m_bModified; }
      }

      public bool DeleteAllocEntryChain(  uint iID  )
      {
         if(  (SysInfo.UNDEFINED == iID) || (!m_Entries.ContainsKey(  iID  ))  )
            return false;
         
         AllocTableEntry aent = (AllocTableEntry)m_Entries[ iID ];

         while(  null != aent  )
         {
            m_Entries.Remove(  aent.m_iID  );
            aent = (AllocTableEntry)m_Entries[ aent.m_iNext ];
         }

         return true;
      }

      public AllocTable.AllocTableEntry GetAllocTableEntryByID(  uint iID  )
      {
         return (AllocTable.AllocTableEntry)m_Entries[ iID ];
      }

      internal void AddAllocTableEntry(  uint iID,  AllocTable.AllocTableEntry ent  )
      {
         m_Entries.Add(  iID,  ent  );
      }
   }
}
